﻿/*
Bf3Rcon.NET, provides a .NET implementation of BF3's RCON interface.
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3rcon.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Net.Sockets;
using System.Text.RegularExpressions;

namespace System.Net.Battlefield3
{
    /// <summary>
    /// The base class used for <see cref="RconClient"/> and <see cref="LightRconClient"/>.
    /// </summary>
    public abstract class RconClientBase : MarshalByRefObject
    {
        #region Private Fields/Properties
        /// <summary>
        /// The size of the buffer used for receiving.
        /// </summary>
        protected internal const int BufferSize = 4096;

        //this is used for the tostrings to satisfy fxcop
        static internal readonly CultureInfo TargetCulture = CultureInfo.GetCultureInfo("en-US");

        //stores what command will be used to get players
        internal string PlayerInfoCommand = "listPlayers";
        #endregion

        #region Ctors
        /// <summary>
        /// Creates a new <see cref="RconClientBase"/> for remotely communicating with a Battlefield 3 server.
        /// </summary>
        protected RconClientBase()
        {
            Players = new PlayerCollection(this);
            Maps = new MapCollection(this);
            Server = new Server(this);
            Bans = new BanCollection(this);
        }
        #endregion

        #region Properties
        // a tcp client is used because it's connect is sometimes faster than a regular socket's, and this is more flexible
        /// <summary>
        /// The <see cref="TcpClient"/> used for socket operations.
        /// </summary>
        protected TcpClient Socket { get; set; }

        /// <summary>
        /// Gets or sets the address that this client will <see cref="Connect()"/> to.
        /// </summary>
        /// <value>The address of the server.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Disconnect()"/> and <see cref="Connect()"/>.</remarks>
        public String Address { get; set; }

        int _Port;
        /// <summary>
        /// Gets or sets the port that this client will <see cref="Connect()"/> to.
        /// </summary>
        /// <value>The port of the server.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Disconnect()"/> and <see cref="Connect()"/>.</remarks>
        /// <exception cref="ArgumentOutOfRangeException">The port must be between 0 and 65535.</exception>
        public int Port
        {
            get { return _Port; }
            set
            {
                if (value < 0 || value > 65535) throw new ArgumentOutOfRangeException("value", "The port must be between 0 and 65535.");
                _Port = value;
            }
        }

        /// <summary>
        /// Gets whether or not the <see cref="RconClientBase"/> is connected to the server.
        /// </summary>
        /// <value>True if the <see cref="RconClientBase"/> has connected to the server.</value>
        public bool IsConnected
        {
            get { return Socket != null && Socket.Connected; }
        }

        /// <summary>
        /// Gets whether or not the <see cref="RconClientBase"/> has logged into the server.
        /// </summary>
        /// <value>True if the <see cref="RconClientBase"/> has logged into the server.</value>
        public bool IsLoggedOn { get; protected set; }

        /// <summary>
        /// Gets the <see cref="PlayerCollection"/> that contains all of the <see cref="Player">Players</see> on the server.
        /// </summary>
        /// <value>A <see cref="PlayerCollection"/> object containing the <see cref="Player">Players</see> in the server.</value>
        public PlayerCollection Players { get; internal set; }

        int _SynchronousReadTimeout = 10;
        /// <summary>
        /// Gets or sets the timeout for synchronous reads, in seconds.
        /// </summary>
        /// <value>The timeout for synchronous reads, in seconds.</value>
        /// <remarks>
        /// Synchronous reads are reads that request and parse/return data in the same method.<para />
        /// By default, the timeout is 10 seconds.<para /><para />
        /// One example of usage is the <see cref="MapCollection"/> class. When getting the maplist, 
        /// <see cref="MapCollection"/> gets the data of three requests, and is able to return data.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must be greater than 0 and less than or equal to <see cref="Int32.MaxValue"/> / 1000.</exception>
        public int SynchronousReadTimeout
        {
            get { return _SynchronousReadTimeout; }

            set
            {
                if (value < 1 || value > (Int32.MaxValue / 1000)) throw new ArgumentOutOfRangeException(
                    "value", "value must be greater than 0 and less than or equal to Int32.MaxValue / 1000.");
                _SynchronousReadTimeout = value;
            }
        }

        /// <summary>
        /// Gets the <see cref="MapCollection"/> object of the <see cref="RconClientBase"/>, allowing interaction with the
        /// server's maplist.
        /// </summary>
        /// <value>The <see cref="RconClientBase">RconClient's</see> <see cref="MapCollection"/>.</value>
        public MapCollection Maps { get; internal set; }

        /// <summary>
        /// Gets the <see cref="System.Net.Battlefield3.Server"/>, which contains various server properties.
        /// </summary>
        /// <value>The <see cref="System.Net.Battlefield3.Server"/> containin server properties.</value>
        public Server Server { get; internal set; }

        /// <summary>
        /// Gets the <see cref="BanCollection"/>, which allows interaction with the server's banlist.
        /// </summary>
        /// <value></value>
        public BanCollection Bans { get; private set; }
        #endregion

        #region Connect, Send
        /// <summary>
        /// Connects the <see cref="RconClientBase"/> to the server.
        /// </summary>
        public abstract void Connect();

        /// <summary>
        /// Disconnects the <see cref="RconClientBase"/> from the server.
        /// </summary>
        public abstract void Disconnect();

        //used for when a disconnect is done for socketexceptions
        /// <summary>
        /// Disconnects the <see cref="RconClientBase"/> whenever a <see cref="SocketException"/> occurs.
        /// </summary>
        /// <param name="message">The message of the <see cref="SocketException"/>.</param>
        /// <param name="sockError">The <see cref="SocketError"/> or the <see cref="SocketException"/>.</param>
        protected internal abstract void Disconnect(string message, SocketError sockError);

        /// <summary>
        /// Sends a <see cref="Packet"/> to the server.
        /// </summary>
        /// <param name="packet">The <see cref="Packet"/> that is being sent.</param>
        protected abstract void SendPacket(Packet packet);

        /// <summary>
        /// Sends a <see cref="Packet"/> to the server.
        /// </summary>
        /// <param name="isFromServer">Whether or not the <see cref="Packet"/> originates from the server.</param>
        /// <param name="isResponse">Whether or not the <see cref="Packet"/> is a response to another one.</param>
        /// <param name="sequence">The sequence number of the <see cref="Packet"/>.</param>
        /// <param name="words">The <see cref="Packet">Packet's</see> words.</param>
        protected void SendPacket(bool isFromServer, bool isResponse, int sequence, params string[] words)
        {
            if (words.Length == 0) return;

            SendPacket(new Packet(isFromServer, isResponse, sequence, words));
        }

        /// <summary>
        /// Sends a response to a <see cref="Packet"/> originally sent by the server.
        /// </summary>
        /// <param name="sequence">The sequence number of the original <see cref="Packet"/>.</param>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        protected void SendResponse(int sequence, params string[] words)
        {
            if (words.Length == 0) return;

            //not sure why isfromserver is true, but that's the way it is in python
            SendPacket(true, true, sequence, words);
        }

        /// <summary>
        /// Sends a request for information that returns the response <see cref="Packet"/>.
        /// </summary>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        /// <returns>The <see cref="Packet"/> set in response to the request or null (Nothing in Visual Basic) if a timeout occurs.</returns>
        /// <remarks>
        /// A timeout will occur when <see cref="SynchronousReadTimeout"/> elapses.
        /// </remarks>
        public abstract Packet SendRequest(params string[] words);

        /// <summary>
        /// Sends a request that came from internal code.
        /// </summary>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        /// <returns>The <see cref="Packet"/> set in response to the request or null (Nothing in Visual Basic) if a timeout occurs.</returns>
        protected internal abstract Packet InternalSendRequest(params string[] words);

        /// <summary>
        /// Sends a PunkBuster server command synchronously.
        /// </summary>
        /// <param name="command">The PunkBuster server command that is to be sent.</param>
        /// <returns>True if the command is sent successfully.</returns>
        /// <remarks>
        /// This method will still return true even if the command isn't a real one.<para />
        /// If the command does not being with "pb_sv_", it will be added automatically. For example, pList will become
        /// pb_sv_pList.
        /// </remarks>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool SendPBCommand(string command)
        {
            if (!IsLoggedOn) throw new NotLoggedOnException();

            //all commands must begin with this
            if (command.Substring(0, 6) != "pb_sv_") command = "pb_sv_" + command;

            return InternalSendRequest("punkBuster.pb_sv_command", command).Success();
        }

        #region admin.say
        //for all, team, and squad, dont convert to subset because it's more efficient to just parse right in method
        /// <summary>
        /// Sends a message to all <see cref="Player">Players</see> in the server.
        /// </summary>
        /// <param name="message">The message to send.</param>
        /// <returns>False if the request fails.</returns>
        public bool SendAllMessage(string message)
        {
            return SendMessage(message, "all");
        }

        /// <summary>
        /// Sends a message to all <see cref="Player">Players</see> in the server on a team.
        /// </summary>
        /// <param name="message">The message to send.</param>
        /// <param name="teamId">The ID of the target team.</param>
        /// <returns>False if the request fails.</returns>
        public bool SendTeamMessage(string message, int teamId)
        {
            return SendMessage(message, "team", teamId.ToStringG());
        }

        /// <summary>
        /// Sends a message to all <see cref="Player">Players</see> in the on a squad of a team.
        /// </summary>
        /// <param name="message">The message to send.</param>
        /// <param name="teamId">The ID of the target team.</param>
        /// <param name="squadId">The ID of the target squad.</param>
        /// <returns>False if the request fails.</returns>
        public bool SendSquadMessage(string message, int teamId, int squadId)
        {
            return SendMessage(message, "squad", teamId.ToStringG(), squadId.ToStringG());
        }

        //this is a helper method for the above methods
        bool SendMessage(string message, string p1, string p2 = "", string p3 = "")
        {
            return InternalSendRequest("admin.say", message, p1, p2, p3).Success();
        }

        /// <summary>
        /// Sends a message to a specified subset of <see cref="Player">Players</see> in the server.
        /// </summary>
        /// <param name="message">The message to send.</param>
        /// <param name="subset">The target subset of <see cref="Player">Players</see>.</param>
        /// <returns>False if the request fails.</returns>
        public bool SendMessage(string message, PlayerSubset subset)
        {
            //do it this way for efficiency
            string[] p = new string[3 + subset.Values.Length];
            p[0] = "admin.say";
            p[1] = message;
            p[2] = subset.Name;
            for (int i = 0; i < subset.Values.Length; i++)
                p[3 + i] = subset.Values[i];

            return InternalSendRequest(p).Success();
        }


        #endregion

        /// <summary>
        /// Logs the <see cref="RconClientBase"/> onto the server.
        /// </summary>
        /// <param name="password">The password for the server's RCON interface.</param>
        /// <return>True if the login is successful or false if the request fails due to timeout.</return>
        public bool LogOn(string password)
        {
            if (!IsLoggedOn)
            {
                if (IsConnected && password.HasValue())
                {
                    Packet p = InternalSendRequest("login.hashed");
                    if (!p.Success()) return false; //timeout

                    Packet p2 = InternalSendRequest("login.hashed", GeneratePasswordHash(password, p.Words[1]));
                    if (p2.Success())
                    {
                        PlayerInfoCommand = "admin.listPlayers";
                        IsLoggedOn = true;
                        return true;
                    }
                    return false;
                }
                return false;
            }
            return true;
        }

        /// <summary>
        /// Generates a password hash from a password and salt.
        /// </summary>
        /// <param name="pass">The password to hash.</param>
        /// <param name="salt">The salt to use in the hash.</param>
        /// <returns>The hash of the password and salt.</returns>
        protected static string GeneratePasswordHash(string pass, string salt)
        {
            string result;
            using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
            {
                byte[] passBytes = Encoding.Default.GetBytes(pass);

                int saltByteCount = salt.Length / 2; //hex string
                byte[] allBytes = new byte[passBytes.Length + saltByteCount];

                for (int i = 0; i < salt.Length; i += 2)
                    allBytes[i / 2] = Convert.ToByte(salt.Substring(i, 2), 16);//converts the hex value into a byte

                for (int i = 0; i < passBytes.Length; i++)
                    allBytes[i + saltByteCount] = passBytes[i];

                result = md5.ComputeHash(allBytes).ToHexString();
            }

            return result.ToUpper(TargetCulture);
        }
        #endregion

        /// <summary>
        /// Gets the IP of a <see cref="Player"/> (by name).
        /// </summary>
        /// <param name="name">The name of the <see cref="Player"/>.</param>
        /// <returns>The IP for the <see cref="Player"/> or null if there is no IP available.</returns>
        protected internal abstract string GetPlayerIP(string name);
    }
}
